package controller

import (
	"context"
	"fmt"
	"strings"
	"time"

	"v8s/compose/model"
	"v8s/logger"

	"v8s-controller/generator"
	"v8s-controller/internal"
	"v8s-controller/internal/configuration"
	"v8s-controller/internal/db"
	"v8s-controller/internal/k8s"
)

type (
	SynchronizationController struct {
		*generic

		database   *db.ProjectsRepository
		kubernetes *k8s.Repository
		generator  *generator.Generator
		properties configuration.Properties
	}

	namespaceIndex map[internal.Namespace]*k8s.Namespace
)

const (
	projectsControllerName = "projects-controller"

	seenTTL = time.Second * 10
)

var (
	withoutStatuses = []db.TaskStatus{db.DeletedTaskStatus, db.SuspendedTaskStatus, db.ErrorTaskStatus}

	skipStatuses                   = withoutStatuses
	createOrPossibleUpdateStatuses = []db.TaskStatus{db.ResumeRequestTaskStatus, db.CreationRequestTaskStatus, db.UpdateRequestTaskStatus}
)

func CreateSynchronizationController(
	database *db.ProjectsRepository,
	kubernetes *k8s.Repository,
	generator *generator.Generator,
	properties configuration.Properties,
) *SynchronizationController {
	return &SynchronizationController{
		generic: &generic{
			log: logger.Logger().Named(projectsControllerName).Sugar(),
		},

		database:   database,
		kubernetes: kubernetes,
		generator:  generator,
		properties: properties,
	}
}

func (p *SynchronizationController) Name() string {
	return projectsControllerName
}

func (p *SynchronizationController) Execute(ctx context.Context) error {
	startedAt := time.Now().Add(-seenTTL)

	p.log.Infof("synchronization of tasks seen before '%s'", startedAt)

	existing, err := p.getExistingNamespaces(ctx)
	if err != nil {
		return err
	}

	return p.withChannel(p.database.ListProjectsWithAllTaskStatusesNotInSince(ctx, 20, withoutStatuses, startedAt), func(project *db.Project) (bool, error) {
		changedTasks := map[internal.TaskID]db.Task{}

		for _, task := range project.Tasks {
			if containsStatus(skipStatuses, task.Status) {
				p.log.Debugf("task status is '%s', skipping processing", task.Status)
				continue
			}

			namespace, err := task.ID.ToNamespace()
			if err != nil {
				return false, fmt.Errorf("failed to convert task id '%s' to namespace: %w", task.ID, err)
			}

			updatedTask, handled, err := p.handle(ctx, task, namespace, existing)
			if err != nil {
				details := fmt.Sprintf("failed to handle task: %s", err)

				p.log.Errorf("failed to handle task '%s': %s", task.ID, details)

				changedTasks[task.ID] = db.Task{ID: task.ID, Status: db.ErrorTaskStatus, StatusDetails: &details}
				continue
			}

			if handled {
				changedTasks[task.ID] = updatedTask
			}
		}

		// всегда сохраняем проект, чтобы снять лок
		return len(changedTasks) > 0, p.database.SaveProjectWithTaskStatuses(ctx, project.DocumentID, changedTasks)
	})
}

func (p *SynchronizationController) handle(ctx context.Context, task db.Task, namespace internal.Namespace, existing namespaceIndex) (db.Task, bool, error) {
	p.log.Debugf("handling task '%s' with status '%s'", task.ID, task.Status)

	if task.Status == db.DeletionRequestTaskStatus {
		if existing[namespace] != nil {
			if err := p.kubernetes.DeleteNamespace(ctx, namespace); err != nil {
				return db.Task{}, false, fmt.Errorf("failed to delete task '%s': %w", task.ID, err)
			}
		}

		p.log.Infof("task deleted in namespace '%s'", namespace)
		return db.Task{ID: task.ID, Status: db.DeletedTaskStatus}, true, nil
	}

	if task.Status == db.RunningTaskStatus {
		deadlineExceed := !task.ModifiedAt.IsZero() && time.Now().After(task.ModifiedAt.Add(p.properties.Task.ProvisioningDeadline))
		allClustersRunning := true
		details := strings.Builder{}

		// if we have 0 Clusters, then watcher didn't find information about task
		if len(task.Clusters) < 1 {
			details.WriteString("- task has 0 clusters\n")
			allClustersRunning = false
		}

		for clusterName, cluster := range task.Clusters {
			if cluster.Status != db.ClusterStatusRunning {
				details.WriteString(fmt.Sprintf("- cluster on cluster '%s' had status '%s'\n", clusterName, cluster.Status))
				for _, resourceInfo := range cluster.Resources {
					if resourceInfo.Status != db.ResourceStatusRunning && resourceInfo.Status != db.ResourceStatusBound {
						details.WriteString(fmt.Sprintf("- Kubernetes resource '%s' named '%s' had status '%s'\n", resourceInfo.Kind, resourceInfo.Name, resourceInfo.Status))
					}
				}
				allClustersRunning = false
				break
			}
		}

		if deadlineExceed && !allClustersRunning {
			details.WriteString(fmt.Sprintf("- task, last modify at '%s', have exceed deadline '%s' of provisioning\n", task.ModifiedAt, p.properties.Task.ProvisioningDeadline))
			p.log.Errorf("task '%s' have exceed deadline of provisioning: %s", task.ID, details.String())
			statusStr := details.String()
			return db.Task{ID: task.ID, Status: db.ErrorTaskStatus, StatusDetails: &statusStr}, true, nil
		}

		handled, err := p.createOrPossiblyUpdateTask(ctx, existing, namespace, task)
		if err != nil {
			return db.Task{}, false, fmt.Errorf("failed to update task '%s': %w", task.ID, err)
		}

		return db.Task{ID: task.ID, Status: db.RunningTaskStatus}, handled, nil
	}

	if containsStatus(createOrPossibleUpdateStatuses, task.Status) {
		handled, err := p.createOrPossiblyUpdateTask(ctx, existing, namespace, task)
		if err != nil {
			return db.Task{}, false, fmt.Errorf("failed to update task '%s': %w", task.ID, err)
		}

		return db.Task{ID: task.ID, Status: db.RunningTaskStatus}, handled, nil
	}

	if task.Status == db.SuspensionRequestTaskStatus {
		if err := p.possiblySuspendTask(ctx, existing, namespace, task); err != nil {
			return db.Task{}, false, fmt.Errorf("failed to update task '%s': %w", task.ID, err)
		}

		return db.Task{ID: task.ID, Status: db.SuspendedTaskStatus}, true, nil
	}

	return db.Task{}, false, fmt.Errorf("unknown status '%s' found in task '%s'", task.Status, task.ID)
}

func (p *SynchronizationController) getExistingNamespaces(ctx context.Context) (namespaceIndex, error) {
	projects, err := p.kubernetes.ListNotDeletedNamespaces(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve projects from kubernetes: %w", err)
	}

	result := map[internal.Namespace]*k8s.Namespace{}
	for _, project := range projects {
		current := project
		result[project.ID] = &current
	}

	return result, nil
}

func (p *SynchronizationController) createOrPossiblyUpdateTask(ctx context.Context, existing namespaceIndex, namespace internal.Namespace, task db.Task) (bool, error) {
	options := make([]generator.Option, 0)
	if p.properties.Feature.LoggingEnabled {
		options = append(options, generator.LoggingOption{LogPrefix: task.RN.StringWithDelimeter('-')})
	}

	resources, err := p.generator.Generate(namespace, model.Compose(*task.Compose), options...)
	if err != nil {
		return false, fmt.Errorf("failed to generate resources: %w", err)
	}

	if existing[namespace] == nil {
		if err := p.kubernetes.CreateResources(ctx, resources); err != nil {
			return false, fmt.Errorf("failed to create task in namespace '%s': %w", namespace, err)
		}

		p.log.Infof("task created in namespace '%s'", namespace)

		return true, nil
	}

	handled, err := p.kubernetes.UpdateResources(ctx, namespace, resources)
	if err != nil {
		return false, fmt.Errorf("failed to update task in namespace '%s': %w", namespace, err)
	}

	if handled {
		p.log.Infof("task updated in namespace '%s'", namespace)
	}

	return handled, nil
}

func (p *SynchronizationController) possiblySuspendTask(ctx context.Context, existing namespaceIndex, namespace internal.Namespace, task db.Task) error {
	if existing[namespace] == nil {
		p.log.Warnf("cannot suspend, namespace '%s' is absent", namespace)
		return nil
	}

	options := make([]generator.Option, 0)
	if p.properties.Feature.LoggingEnabled {
		options = append(options, generator.LoggingOption{LogPrefix: task.RN.StringWithDelimeter('-')})
	}

	resources, err := p.generator.Generate(namespace, *task.Compose, options...)
	if err != nil {
		return fmt.Errorf("failed to generate suspended resources for namespace '%s': %w", namespace, err)
	}

	_, err = p.kubernetes.UpdateResources(ctx, namespace, resources)
	if err != nil {
		return fmt.Errorf("failed to suspend task in namespace '%s': %w", namespace, err)
	}

	p.log.Infof("task suspended in namespace '%s'", namespace)

	return nil
}

func containsStatus(statuses []db.TaskStatus, target db.TaskStatus) bool {
	for _, status := range statuses {
		if status == target {
			return true
		}
	}
	return false
}
